package com.xiejr.aspect;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.xiejr.annotations.BusinessLog;
import com.xiejr.context.ApplicationContextHolder;
import com.xiejr.entity.SysLog;
import com.xiejr.entity.SysLogExt;
import com.xiejr.service.SysLogService;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.Order;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.util.StopWatch;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/**
 * @program: hxzq
 * @description: 业务日志全局切面,切面优先级最低，保证日志不影响其他业务功能
 * @author: xjr
 * @create: 2020-11-11 11:41
 **/
@Aspect
@Slf4j
@Configuration
@Order(101)
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@ConditionalOnClass(MybatisPlusProperties.class)
public class BusinessLogAspect {

    private static final String LOG_POINTCUT="@annotation(com.xiejr.annotations.BusinessLog)";

    /**
     *存放方法执行前后的log，用来额外保存一些信息，后置动作需要手动remove，否则存在线程池的重复使用可能造成插入脏数据
     */
    private static final ThreadLocal<SysLog> SYS_LOG_THREAD_LOCAL=ThreadLocal.withInitial(SysLog::new);

    /**
     *el表达式支持
     */
    private static final ExpressionParser EXPRESSION_PARSER=new SpelExpressionParser();

    /**
     *el表达式支持
     */
    private static final EvaluationContext EVALUATION_CONTEXT=new StandardEvaluationContext();




    private final ThreadPoolTaskExecutor threadPoolTaskExecutor;


    private final SysLogService sysLogService;


    @Pointcut(LOG_POINTCUT)
    public void pointCut(){
    }

    @Before("pointCut()")
    public void before(JoinPoint joinpoint){
        final HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        SysLog sysLog = SYS_LOG_THREAD_LOCAL.get();
        sysLog.setIpAddr(ApplicationContextHolder.get(ApplicationContextHolder.IP));
        sysLog.setOpName("admin");
        SysLogExt sysLogExt=new SysLogExt();
        StopWatch clock=new StopWatch();
        clock.start(joinpoint.getSignature().getName());
        sysLogExt.setClock(clock);
        sysLog.setExt(sysLogExt);
    }






    private Map<String, Object> getFieldsName(JoinPoint joinPoint) throws Exception {
        String classType = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        // 参数值
        Object[] args = joinPoint.getArgs();
        Class<?>[] classes = new Class[args.length];
        for (int k = 0; k < args.length; k++) {
            // 对于接受参数中含有MultipartFile，ServletRequest，ServletResponse类型的特殊处理，我这里是直接返回了null。（如果不对这三种类型判断，会报异常）
            if (args[k] instanceof MultipartFile || args[k] instanceof ServletRequest || args[k] instanceof ServletResponse) {
                return null;
            }
            if (!args[k].getClass().isPrimitive()) {
                // 当方法参数是基础类型，但是获取到的是封装类型的就需要转化成基础类型
//                String result = args[k].getClass().getName();
//                Class s = map.get(result);

                // 当方法参数是封装类型
                Class s = args[k].getClass();

                classes[k] = s == null ? args[k].getClass() : s;
            }
        }
        ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
        // 获取指定的方法，第二个参数可以不传，但是为了防止有重载的现象，还是需要传入参数的类型
        Method method = Class.forName(classType).getMethod(methodName, classes);
        // 参数名
        String[] parameterNames = pnd.getParameterNames(method);
        // 通过map封装参数和参数值
        HashMap<String, Object> paramMap = new HashMap();
        for (int i = 0; i < parameterNames.length; i++) {
            paramMap.put(parameterNames[i], args[i]);
        }
        return paramMap;
    }

    @Around("pointCut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        SysLog sysLog=SYS_LOG_THREAD_LOCAL.get();

        try {
            Object proceed = pjp.proceed();
            sysLog.setRespParam(JSON.toJSONString(proceed).getBytes(StandardCharsets.UTF_8));
            sysLog.setLogType(0);
            return proceed;
        }catch (Throwable e){
            //记录异常日志
            sysLog.setLogType(1);
            log(pjp);
            throw new Throwable(e.getMessage());
        }

    }


    /**
     *
     * @param  joinPoint 切入点
     * @return void
     * @author xiejr
     * @description: 方法正常执行后执行日志记录操作
     * @date 2020/11/11 16:07
     */

    @AfterReturning("pointCut()")
    public void afterReturning(JoinPoint joinPoint){
        log(joinPoint);
    }


    public void log(JoinPoint joinPoint){
        SysLog sysLog = SYS_LOG_THREAD_LOCAL.get();
        LocalDateTime now = LocalDateTime.now();
        sysLog.setCtrTime(now);
        sysLog.setUpdTime(now);
        StopWatch clock = sysLog.getExt().getClock();
        clock.stop();
        sysLog.setExecTime(clock.getLastTaskTimeMillis());
        final HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        SYS_LOG_THREAD_LOCAL.remove();
        //采用异步编程,避免出现异常导致其他的像事务造成回滚,提高响应速度
        threadPoolTaskExecutor.execute(()->{
            MethodSignature methodSignature= (MethodSignature) joinPoint.getSignature();
            Method execMethod=methodSignature.getMethod();
            BusinessLog execAnnotation = execMethod.getAnnotation(BusinessLog.class);
            sysLog.setLevel(execAnnotation.level());
            sysLog.setOpType(execAnnotation.type().getCode());
            //TODO 加入el表达式支持
            String title=execAnnotation.value();
            //日志标题优先取注解的value，为空则根据swagger注解进行插入，若开发没有使用swagger，则通过注解的执行动作和操作数据表进行组装
            if (StringUtils.isBlank(execAnnotation.value())){
                ApiOperation operation=execMethod.getAnnotation(ApiOperation.class);
                if (operation!=null && StringUtils.isNotBlank(operation.value())){
                    title=operation.value();
                }else{
                    title=String.format("%s表%s",execAnnotation.table(),execAnnotation.type().getMsg());
                }
            }
            if (execAnnotation.activeEL()){
                synchronized (EXPRESSION_PARSER){
                    try {
                        setElValByParam(getFieldsName(joinPoint));
                        Expression expression = EXPRESSION_PARSER.parseExpression(title, new TemplateParserContext());
                        title=expression.getValue(EVALUATION_CONTEXT,String.class);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }

            sysLog.setTitle(title);
            sysLog.setIsDelete(0);
            sysLog.setOpModel(execAnnotation.table());
            sysLog.setUri(request.getRequestURI());
            sysLog.setBrowser(Optional.ofNullable(request.getHeader("User-Agent")).map(Object::toString).map(data->{
                String[] s = data.split(" ");
                return s[0];
            }).orElse(""));
            try {
                sysLog.setReqParam(JSON.toJSONString(getFieldsName(joinPoint)).getBytes(StandardCharsets.UTF_8));
            } catch (Exception e) {
                sysLog.setReqParam(null);
            }

            SysLogExt sysLogExt=sysLog.getExt();
            sysLogExt.setExecMethod(execMethod);
            sysLogExt.setTargetClass(joinPoint.getTarget().getClass());
            sysLogService.save(sysLog);
        });
    }

    /**
     *
     * @param param 切点入参
     * @return void
     * @author xiejr
     * @description: 组装el表达式上下文变量
     * @date 2020/11/12 10:28
     */

    private void setElValByParam(Map<String,Object> param){
      if (!param.isEmpty()){
          param.forEach((k,v)->{
              Class<?> clazz=v.getClass();
              if (!(v instanceof Collection || v instanceof Map || clazz.isArray())){
                  EVALUATION_CONTEXT.setVariable(k,v);
              }
          });
      }
    }


}
